package emulator.hardware.io.peripherals.iec;

import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Observer;

import emulator.hardware.HwByte;
import emulator.hardware.clock.Clock;
import emulator.hardware.clock.ClockHandle;
import emulator.hardware.io.peripherals.SerialPort;

public class IecSim extends Thread {
	
	private static final int RCVBUF_MAX = 65536;
	private ClockHandle clock = null;
	protected SerialPort port = null;
	private IecLog log = null;
	private boolean is_state_log_enabled = false;
	private ByteBuffer buffer = ByteBuffer.allocate(RCVBUF_MAX);
	private HashMap<Integer, IecSimDevice> device_map =
				new HashMap<Integer, IecSimDevice>();
	IecSimDevice active_device = null;
	private boolean has_to_talk = false;

	public IecSim()
	{
		super();
		log = new IecLog();
		
		addDevice(new IecSimNullPrinter(), 4);
		addDevice(new IecSimFsDriver(), 8);
//		addDevice(new IecSimFsDriver(), 9);
//		addDevice(new IecSimFsDriver(), 10);
//		addDevice(new IecSimFsDriver("../vin"), 11);
	}

	private void addDevice(IecSimDevice device, int address) {
		device_map.put(address, device);
		device.setIecSim(this);
	}
	
	public void attach(Clock clock, SerialPort port)
	{
		if (clock != null)
			this.clock = clock.acquireHandle();
		this.port = port;
	}
	
	public void detach()
	{
		this.clock = null;
	}

	@Override
	public void run() {
		releaseLine();
		logState("Init");
		for (;;)
		{
			try {
				clock.tick();
				if (port.getAtn() == false)
					doAtn();
				if (port.getClk() == false
						&& active_device != null)
					doClk();
				if (has_to_talk)
					doTalk();
			}
			catch (TimeoutException e)
			{
				logState("TOT");
			}
		}
	}

	private void doTalk() throws TimeoutException {
		has_to_talk = false;
		logState("TLK");
		int data = active_device.read();
		if (data < 0)
		{	// no data to send
			logState("NOD");
			port.setBusClk(false);
			clock.tick(30);
			releaseLine();
			return;
		}
		waitClockLine(true, 100);
		port.setBusData(true);
		if (port.getData() != false)
		{
			logState("NDT");
			return;
		}
		int data_next = -1;
		for (;data >= 0; data = data_next)
		{
			data_next = active_device.read();
			port.setBusClk(false);
			clock.tick(80);
			port.setBusClk(true);
			logState("WSD");
			waitDataLine(true, 0);
			if (port.getAtn() == false)
			{
				active_device.rewind(2);
				releaseLine();
				return;
			}
			if (data_next < 0)
			{	// Send EOI
				logState("EOS");
				clock.tick(200);
				logState("WAK");
				waitDataLine(false, 0);
				waitDataLine(true, 0);
			}
			logState("SND");
			reportByte("<-", false, data);
			for (int i = 0; i < 8; i++)
			{
				port.setBusClk(false);
				port.setBusData((data & (1 << i)) != 0);
				logState("BIT"+i);
				clock.tick(70);
				port.setBusClk(true);
				clock.tick(50);
			}
			port.setBusClk(false);
			port.setBusData(true);
			waitDataLine(false, 1000);
			logState("ACK");
		}
		logState("ESD");
		releaseLine();
	}

	private void doAtn() throws TimeoutException {
		logState("ATN");
		port.setBusData(false);
		waitClockLine(false, 0);
		receive(true);
	}
	
	private void doClk() throws TimeoutException {
		logState("CLK");
		port.setBusData(false);
		receive(false);
	}
	
	private void receive(boolean is_atn) throws TimeoutException {
		buffer.clear();
		DataReceiver receiver = new DataReceiver();
		logState("RCV");
		if (is_atn)
		{
			if (!receiver.receiveByte(is_atn, this))
			{
				releaseLine();
				logState("PIN");
				return;
			}
			boolean is_device_listener = checkCommand(receiver);
			if (!is_device_listener)
			{
				releaseLine();
				logState("NOL");
				while (port.getAtn() == false)
					clock.tick();
				return;
			}
			// ACK if active
			port.setBusData(false);
		}
		if (active_device == null)
		{
			releaseLine();
			return;
		}
		while (!receiver.getEoi() && port.getAtn() != is_atn)
		{
			if (!receiver.receiveByte(is_atn, this))
				break;
			buffer.put((byte) receiver.getData());
			port.setBusData(false);
		}
//		clock.tick(70);
		logState("RLS");
		try { 
			waitClockLine(true, 80); 
		}
		catch (TimeoutException t)
		{}
		releaseLine();
		logState("FIN");
		buffer.flip();
		if (buffer.hasRemaining());
			processData(is_atn);
	}

	private void processData(boolean is_atn) {
		if (active_device != null)
		{
			if (is_atn)
				active_device.processCommand(buffer);
			else 
				active_device.processData(buffer);
		}
	}

	private boolean checkCommand(DataReceiver receiver) {
		int cmd = receiver.getData();
		boolean is_device_listener = false;
		if ((cmd & 0xE0) == 0x20 || (cmd & 0xE0) == 0x40)
		{
			if (cmd == 0x3f || cmd == 0x5f) // UNLISTEN/UNTALK
			{
				if (active_device != null)
				{
					if (cmd == 0x3f)
						active_device.unlisten();
					else
						active_device.untalk();
					active_device = null;
					is_device_listener = true;
				}
			}
			else // LISTEN/TALK
			{
				active_device = null;
				int address = cmd & 0x1F;
				if (device_map.containsKey(address))
				{
					active_device = device_map.get(address);
					if ((cmd & 0xE0) == 0x20)
						active_device.listen();
					else
						active_device.talk();
					is_device_listener = true;
				}
			}
		}
		return is_device_listener;
	}

	private void releaseLine() {
		port.setBusData(true);
		port.setBusClk(true);
	}

	protected void reportByte(String dir, boolean is_atn, int data) {
		String log_string
			= dir + " "
			+ (is_atn ? "/" : " ") + new HwByte(data)
			+ " - ";
		if (is_atn)
		{
			if (data >= 0x20 && data < 0x3F)
				log_string += "LISTEN #" + (data & 0x1F);
			else if (data == 0x3F)
				log_string += "UNLISTEN";
			else if (data >= 0x40 && data < 0x5F)
				log_string += "TALK #" + (data & 0x1f);
			else if (data == 0x5F)
				log_string += "UNTALK";
			else if (data >= 0x60 && data < 0x70)
				log_string += "OPEN CHN/DATA #" + (data & 0x0f);
			else if (data >= 0xE0 && data < 0xF0)
				log_string += "CLOSE #" + (data & 0x0f);
			else if (data >= 0xF0)
				log_string += "OPEN #" + (data & 0x0f);
			else
				log_string += "???";
						
		}
		else if (data >= 32 && data < 128)
			log_string += "\'" + (char)data + "\'";
		log(log_string);
	}

	protected boolean waitClockOrAtn(boolean result, boolean is_atn) {
		while(port.getClk() != result && port.getAtn() != is_atn)
			clock.tick();
		return port.getAtn() == is_atn;
	}

	protected void signalAck() {
		logState("ACK");
		port.setBusData(false);
		clock.tick(80);
		port.setBusData(true);
	}

	protected int waitClockLine(boolean result, int timeout) throws TimeoutException {
		int time = 0;
		while(port.getClk() != result)
		{
			clock.tick();
			if (++time == timeout)
				throw new TimeoutException();
		}
		return time;
	}

	private int waitDataLine(boolean result, int timeout) throws TimeoutException {
		int time = 0;
		while(port.getData() != result)
		{
			clock.tick();
			if (++time == timeout)
				throw new TimeoutException();
		}
		return time;
	}

	protected void logState(String text) {
		if (is_state_log_enabled)
		{
			String state =
				"A:" + (port.getAtn() ? "T" : "F") +
				" C:" + (port.getClk() ? "T" : "F") +
				" D:" + (port.getData() ? "T" : "F") +
				" - " + text;
			log(state);
		}
	}

	void log(String line)
	{
		log.writeln(line);
	}

	public void addLogObserver(Observer o)
	{
		log.addObserver(o);
	}

	public void deleteLogObserver(Observer o)
	{
		log.deleteObserver(o);
	}

	public String getLogLine() {
		return log.getLogLine();
	}

	public void setStateLogEnabled(boolean enabled) {
		is_state_log_enabled = enabled;
	}
	
	public boolean isStateLogEnabled()
	{
		return is_state_log_enabled;
	}

	public void initSend(IecSimDevice device) {
		has_to_talk = true;
	}

	public Object getDevices() {
		// TODO Auto-generated method stub
		return null;
	}
}
